package cn.redragon.soa.aspect;


import static cn.redragon.soa.common.exception.StockError.DUPLICATED_ACTION;
import static cn.redragon.soa.common.exception.StockError.DUPLICATED_RESERVATION;
import static cn.redragon.soa.common.exception.StockError.MISSING_RESERVATION;

import cn.redragon.soa.common.annotation.CheckReservation;
import cn.redragon.soa.common.constant.Constants;
import cn.redragon.soa.common.constant.RedisCacheKey;
import cn.redragon.soa.common.exception.ServiceException;
import cn.redragon.soa.common.exception.SystemError;
import cn.redragon.soa.common.service.RedisService;
import cn.redragon.soa.common.util.JsonUtil;
import cn.redragon.soa.common.web.ResponseDTO;
import cn.redragon.soa.model.dto.stock.ReservationDTO;
import com.google.common.collect.Maps;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CheckReservationAspect {

    private static final String PARAM_BRAND = "appCode";
    private static final String PARAM_STOCK_TYPE = "type";
    private static final String PARAM_CUSTOMER_ID = "customerId";
    private static final String PARAM_ORDER_NUMBER = "orderNumber";
    private static final String PARAM_RESERVATION = "reservationDTO";

    private final RedisService redisHelper;

    @Pointcut(
        value = "execution(* cn.redragon.soa.controller.ReservationController.*(..)) && @annotation(cn.redragon.soa.stock.aop.CheckReservation)")
    public void checkReservationPointCut() {
        //empty block
    }

    @Around(value = "checkReservationPointCut()")
    public Object checkAspect(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String key = null;
        String lockKey = null;
        boolean locked = false;
        String field = null;
        Object result = null;
        boolean needReservation = false;
        ReservationDTO newReservation = null;
        try {
            Class<?> clazz = proceedingJoinPoint.getTarget().getClass();
            Method method = getMethod(proceedingJoinPoint, clazz);
            needReservation = method.getAnnotation(CheckReservation.class).required();
            Object[] arguments = proceedingJoinPoint.getArgs();
            Map<String, Object> params = this.assembleRequestParam(method, arguments);
            String appCode = (String) params.get(PARAM_BRAND);
            String customerId = (String) params.get(PARAM_CUSTOMER_ID);
            String orderNumber = (String) params.get(PARAM_ORDER_NUMBER);
            String stockType = (String) params.get(PARAM_STOCK_TYPE);
            if (appCode == null || customerId == null || orderNumber == null || stockType == null) {
                log.error("Missing request parameters appCode or payload in method signature");
                throw new IllegalArgumentException(SystemError.INPUT_CONVERT_ERROR.getMessage());
            }
            newReservation = (ReservationDTO) params.get(PARAM_RESERVATION);
            key = String.format(RedisCacheKey.MA_CUSTOMER_PROFILE, appCode, customerId);
            field = String.format(RedisCacheKey.HASH_ORDER_RESERVED, stockType, orderNumber);

            boolean hasReservation = BooleanUtils.isTrue(redisHelper.hashExists(key, field));
            if (!needReservation && hasReservation) {
                throw new ServiceException(DUPLICATED_RESERVATION, stockType, orderNumber, customerId);
            } else if (needReservation && !hasReservation) {
                throw new ServiceException(MISSING_RESERVATION, stockType, orderNumber, customerId);
            }
            lockKey = String.format(RedisCacheKey.RESERVATION_LOCK, appCode, stockType, orderNumber);
            locked = redisHelper.acquireLock(lockKey, Boolean.TRUE.toString(), Duration.ofSeconds(
                Constants.TtlTime.TTL_1_MIN));
            if (locked) {
                log.debug("Succeeded in acquiring reservation lock by key {}", lockKey);
                result = proceedingJoinPoint.proceed();
                return result;
            } else {
                log.debug("Failed to acquire reservation lock by key {}", lockKey);
                throw new ServiceException(DUPLICATED_ACTION, stockType, orderNumber, customerId);
            }
        } finally {
            if (locked) {
                updateReservationAndReleaseLock(key, lockKey, field, newReservation, result, needReservation);
            }
        }
    }

    private void updateReservationAndReleaseLock(final String key, final String lockKey, final String field, final ReservationDTO newReservation,
                                                 final Object result, final boolean needReservation) {
        boolean respOK = isSuccessResponse(result);
        if (respOK && !needReservation && newReservation != null) {
            // cache reservation in order to cancel or close in future
            redisHelper.hashSet(key, field, JsonUtil.objectToJsonStr(newReservation));
        } else if (respOK) {
            // reservation is cancelled or closed, remove cache
            redisHelper.hashDel(key, field);
        }

        redisHelper.releaseLock(lockKey, Boolean.TRUE.toString());
        log.debug("Reservation lock released for key {}", lockKey);
    }

    private boolean isSuccessResponse(final Object result) {
        return result instanceof ResponseDTO && ResponseDTO.OK.equals(((ResponseDTO) result).getCode());
    }

    private Map<String, Object> assembleRequestParam(Method method, Object[] argsValue) {
        StandardReflectionParameterNameDiscoverer u = new StandardReflectionParameterNameDiscoverer();
        String[] argsName = u.getParameterNames(method);
        Map<String, Object> map = Maps.newHashMap();
        if (ArrayUtils.isEmpty(argsName) || ArrayUtils.isEmpty(argsValue)) {
            return map;
        }
        for (int i = 0; i < argsName.length; i++) {
            map.put(argsName[i], argsValue[i]);
        }
        return map;
    }

    private Method getMethod(JoinPoint point, Class<?> clazz) throws NoSuchMethodException {
        String method = point.getSignature().getName();
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
        return clazz.getMethod(method, parameterTypes);
    }
}
